package cld_client;

use English;
use Socket;
use IO::Select;
use JSON;
use Data::Dumper;
use IO::Socket::INET;
use Digest::MD5 qw(md5_hex);
use Crypt::CBC;
use Crypt::Blowfish;
use Net::Ping;
use Carp qw(cluck);
use clnet qw(CLNET_return_static_ips2);
use strict;

BEGIN {
	if(!exists($::ENV{LINUXHA_LIBS})) {
		$::ENV{LINUXHA_LIBS}="/opt/linuxha14/lib/perl";
	}
	push @INC,$::ENV{LINUXHA_LIBS};
}


# IPv6 support requires the standard IO::Socket::INET6 to be
# available and our own attempt at a 'ping6' hack.
eval 'use IO::Socket::INET6';
# eval 'use Socket6;';
if($@) {
	$cld_client::ip6=0;
	$cld_client::ping_ip6=0;
} else {
	$cld_client::ip6=1;
	use ping6;
}

use constant PF_PACKET => 17; # not defined as yet!
# use constant SOCK_PACKET => 10; 
$cld_client::last_error=undef;
$cld_client::last_rc=0;

#########################################################################
# The new method creates a new client object, and requires the		#
# following arguments:							#
# KEY	The key used to encode/decode packets.				#
# INET	The internet address to connect to.				#
# PORT	The port number to connect to.					#
# TYPE		- Mandatory now...					#
#		  4 - ip4, 6 = ip6, both = ip6				#
# [ILIST] Complete list of internet addresses to use - comma		#
#	  separated. If specified 					#
#         each connection attempt will ping first and use what		#
#	  INET was, otherwise it will use an alternative for this	#
#	  connection - timeout is .25 seconds...			#
#	  If no responses returns undef when a message is sent.		#
#	  Prefix of 4| or IP4| or 6| IP6| on each address to ping	#
#	  IP6 addresses...						#
#########################################################################

sub new {
my $proto=shift;
my %args=@_;
my $class=ref($proto) || $proto;
my $self={};

	if(!exists($args{KEY}) || !exists($args{PORT}) ||
		!exists($args{INET}) || !exists($args{TYPE})) {
		$cld_client::last_error="Missing function arguments [key,port,inet,type].";
		$cld_client::last_rc=1;
		return undef;
	}
	if(!exists($args{ILIST}) && $args{INET} !~ /^\d+\./ && $args{INET} !~ /:/) {
		my @i4=();
		my @i6=();
		my @i=CLNET_return_static_ips2(node=>$args{INET});
		for my $ci (@i) {
			if($ci =~ /\./) {
				push @i4,$ci;
			} else {
				my $x=$ci;
				if(substr($x,0,1) eq '[') {
					$x=substr($x,1,length($x)-2);
				}
				push @i6,$x;
			}
		}
		$self->{ILIST}=[@i4];
		$self->{ILIST6}=[@i6];
	}
	if(exists($args{ILIST}) && defined($args{ILIST})) {
		my @L=split(/,/,$args{ILIST});
		my @I4=(); my @I6=();
		for my $c (@L) {
			if($c =~ /:/) {
				if(substr($c,0,1) eq '[') {
					$c=substr($c,1,length($c)-2);
				}
				push @I6,$c;
			} else {
				push @I4,$c;
			}
		}
		$self->{ILIST}=[@I4];
		$self->{ILIST6}=[@I6];
	}
	$args{TYPE}=6 if $args{TYPE} eq 'both';
	if($args{TYPE}==6 && !  $cld_client::ip6) {
		$cld_client::last_error="IPv6 functionality requested, but not available (no IO::Socket::INET6).";
		$cld_client::last_rc=1;
		return undef;
	}
	$self->{KEY}=$args{KEY};
	$self->{HOST}=$args{INET};
	$self->{INET}=$args{INET};
	$self->{TYPE}=$args{TYPE};
	if($args{TYPE}!=6) {
		$self->{PING}=Net::Ping->new("icmp");
	}
	$self->{PORT}=$args{PORT};
	$self->{SOCKET}=undef;
	$self->{CRYPT}=Crypt::CBC->new({
		key => $args{KEY},
		cipher => 'Blowfish',
		});
	bless($self,$class);
	return $self;
}

#########################################################################
# This method will connect to the server, send the message and return	#
# the response.								#
# The message to send is in the MSG argument.				#
# It can be called with the connection open , or closed.		#
# Takes optional SENDONLY argument.					#
#########################################################################

sub send_msg {
my $self=shift;
my %args=@_;
my $selector;
my ($md5,$encoded_msg,$fd,$old_fd);
my ($md52,$decoded_msg,$server_response,$plain_text);
my (@sockets,$sock);
my $timeout=5;
my $sendonly=0;

	$sendonly=$args{SENDONLY} if exists($args{SENDONLY});
	if(exists($args{TIMEOUT})) {
		$timeout=$args{TIMEOUT};
	}
	if(!exists($args{MSG})) {
		$cld_client::last_error="Missing function arguments";
		$cld_client::last_rc=1;
		cluck("Missing MSG argument.");
		return undef;
	}

	if(!defined($args{MSG})) {
		confess("Yuck - undefined message passed.");
	}
	$md5=md5_hex($args{MSG});
	$encoded_msg=$self->{CRYPT}->encrypt_hex($args{MSG});

	#################################################################
	# If the socket is not open, then we now open it.		#
	#################################################################

	if(!defined($self->{SOCKET})) {
		#########################################################
		# Notice that ILIST and PING are args now to ensure	#
		# we use the ip4 or ip6 versions depending on the TYPE	#
		# setting of the client.				#
		# If ILIST then always verify connection first.		#
		#########################################################
		my $ilist_t="ILIST";
		if($self->{TYPE}==6) {
			$ilist_t="ILIST6";
		}
		if(exists($self->{$ilist_t})) {
			my $ok=1;
			my $sip=$self->{HOST};
			$ok=0;
			my $start_delay=0.25;
			my $total_time=0;
			my $cip;
			if(! @{$self->{$ilist_t}}) {
				$cld_client::last_error="Not able to find IPv$self->{TYPE} for $self->{HOST}.";
				$cld_client::last_rc=1;
				cluck("Not able to find IPv$self->{TYPE} for $self->{HOST}.");
				return undef;
			}
			while(1) {
				for $cip (@{$self->{$ilist_t}}) {
					if($self->{TYPE}!=6) {
						if($self->{PING}->ping($cip,$start_delay)) {
							$self->{HOST}=$cip;
							$ok=1;
							last;
						}
					} else {
						my $pp=ping6($cip,$start_delay);
						if($pp==1) {
							$self->{HOST}=$cip;
							$ok=1;
							last;
						}
					}
					$total_time+=$start_delay;
				}
				last if $ok;
				if($total_time>$timeout) {
					last;
				}
				$start_delay+=0.25;
			}
			if(! $ok) {
				$cld_client::last_error="Unable to connect to server on any IP address!";
				$cld_client::last_rc=2;
				return undef;
			}
		}
		#  14.09.2011 - Use IO::Socket::INET[6] instead
		# to make it easier to handle ipv4 and ipv6 connections.

		if($self->{TYPE}==4) {
			# my $proto=getprotobyname('tcp');
			# if(! socket($fd,PF_INET,SOCK_STREAM,$proto)) {
				# $cld_client::last_error="Failed to establish connection to server (connect).";
				# $cld_client::last_rc=4;
				# return undef;
			# }
			# my $sin=sockaddr_in($self->{PORT},inet_aton($self->{HOST}));
			# if(!connect($fd,$sin)) {
				# $cld_client::last_error="Failed to establish connection to server (connect).";
				# $cld_client::last_rc=4;
				# return undef;
			# }
			
			$fd=IO::Socket::INET->new(
				Proto => 'tcp',
				PeerPort => $self->{PORT},
				PeerAddr => $self->{HOST},
				Timeout => $timeout
			);
		} else {
			$fd=IO::Socket::INET6->new(
				Domain => AF_INET6,
				Proto => 'tcp',
				PeerPort => $self->{PORT},
				PeerAddr => $self->{HOST},
				Timeout => $timeout
			);
			# my ($fam,$stype,$tcp,$saddr,$cname);
			# my @res=getaddrinfo($self->{HOST},$self->{PORT},AF_UNSPEC,SOCK_STREAM);
			# die("Failed to get IPv6 info for connection!") unless @res;
			# $fam=0;
			# ($fam,$stype,$tcp,$saddr,$cname,@res) = @res while($fam != AF_INET6);
			# die("IPv6 not supported by host.") unless ($fam == AF_INET6);
			# # my $proto=getprotobyname('tcp');
			# # my $saddr=pack_sockaddr_in6($self->{PORT},
				# # inet_pton(AF_INET6,$self->{HOST})) or die;
			# if(! socket($fd,$fam,$stype,$tcp)) {
				# $fd=undef;
				# $cld_client::last_error="Failed to establish connection to server (socket).";
				# $cld_client::last_rc=4;
				# return undef;
			# }
			# if(! connect($fd,$saddr)) {
				# close($fd);
				# $fd=undef;
				# $cld_client::last_error="Failed to establish connection to server (connect).";
				# $cld_client::last_rc=4;
				# return undef;
			# }
		}
		if(! $fd) {
			$cld_client::last_error="Failed to establish connection to server (connect).";
			$cld_client::last_rc=4;
			return undef;
		}
		$self->{SOCKET}=$fd;

		#########################################################
		# Set autoflush on the socket to ensure our message	#
		# is sent and thus responded to as we expect.		#
		#########################################################

		$old_fd=select($fd);
		$|=1;
		select($old_fd);
	} else {
		$fd=$self->{SOCKET};
	}

	#################################################################
	# Send the message, checking for any problems...		#
	#################################################################

	my $x;
	eval {
		local $SIG{ALRM} = sub { die "alarm clock restart" };
		alarm $timeout;
		$x=print $fd "$md5,$encoded_msg\n";
		alarm 0;
	};
	if ($@ and $@ =~ /alarm clock restart/) {
		$cld_client::last_error="Timeout whilst writing message to server";
		$cld_client::last_rc=4;
		close($fd);
		# $self->{SOCKET}->shutdown(2);
		$self->{SOCKET}=undef;
		$fd=undef;
		return undef;
	}

	#################################################################
	# Return a dummy OK response if we only want to send out the	#
	# message and not wait for a reply.				#
	#################################################################
	if($sendonly) {
		close($fd);
		# $self->{SOCKET}->shutdown(2);
		$self->{SOCKET}=undef;
		return "OK";
	}
	if(!$x) {
		$cld_client::last_error="Unable to write message to server";
		$cld_client::last_rc=3;
		close($fd);
		# $self->{SOCKET}->shutdown(2);
		$self->{SOCKET}=undef;
		$fd=undef;
		return undef;
	}

	#################################################################
	# Now the message has been sent, we must await the response.	#
	#################################################################

	eval {
		local $SIG{ALRM} = sub { die "alarm clock restart" };
		alarm $timeout;
		$server_response=<$fd>;
		alarm 0;
	};

	if ($@ and $@ =~ /alarm clock restart/) {
		$cld_client::last_error="Timeout whilst waiting for message from server";
		$cld_client::last_rc=4;
		close($fd);
		# $self->{SOCKET}->shutdown(2);
		$self->{SOCKET}=undef;
		return(undef);
	}

	if(! defined($server_response)) {
		$cld_client::last_error="Broken socket before client request completed (incorrect key?).";
		$cld_client::last_rc=4;
		close($fd);
		# $self->{SOCKET}->shutdown(2);
		$self->{SOCKET}=undef;
		return undef;
	}
	chomp($server_response);
	($md5,$decoded_msg)=($server_response =~ /([A-Fa-f0-9]+),(.*)/);
	if(!defined($md5) || !defined($decoded_msg)) {
		$cld_client::last_error="Invalid message format.";
		$cld_client::last_rc=5;
		close($fd);
		# $self->{SOCKET}->shutdown(2);
		$self->{SOCKET}=undef;
		return undef;
	}

	$plain_text=$self->{CRYPT}->decrypt_hex($decoded_msg);
	if(!defined($plain_text)) {
		$cld_client::last_error="Invalid message format.";
		$cld_client::last_rc=6;
		close($fd);
		# $self->{SOCKET}->shutdown(2);
		$self->{SOCKET}=undef;
		return undef;
	}
	$md52=md5_hex($plain_text);
	if($md52 ne $md5) {
		$cld_client::last_error="Invalid encryption key used for message.";
		$cld_client::last_rc=7;
		close($fd);
		# $self->{SOCKET}->shutdown(2);
		$self->{SOCKET}=undef;
		return undef;
	}
	close($fd);
	# $self->{SOCKET}->shutdown(2);
	$self->{SOCKET}=undef;
	my $ss=decode_json($plain_text);
	return $ss->{data};
}

#########################################################################
# If the specified connection is open, thn we attempt to close it.	#
#########################################################################

sub close_session {
my $self=shift;

	if(defined($self->{SOCKET})) {
		# $self->{SOCKET}->close;
		close($self->{SOCKET});
		$self->{SOCKET}=undef;
	}
}

sub get_error {
	return ($cld_client::last_error,$cld_client::last_rc);
}

1;
